/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

public class ParamNameResolver {

  private static final String GENERIC_NAME_PREFIX = "param";

  /**
   * <p>
   * The key is the index and the value is the name of the parameter.<br />
   * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
   * the parameter index is used. Note that this index could be different from the actual index
   * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
   * </p>
   * <ul>
   * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
   * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
   * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
   * </ul>
   *
   * 凡是加了@Param注解的会单独处理，特殊参数也会单独处理
   */

  // 方法入参的参数次序表。键为参数次序，值为参数名称或者参数@Param注解的值
  // lynch 这里的是  1，schoolName 在构造方法的时候完成这个属性值的构造
   //lynch 这里其实是非常关键的，即限定了key的一个顺序，为什么会关心参数的位置，因为可以输入的参数中有可能出现RowBounds或者ResultHandler等不需要绑定关系的参数，会被跳过
  private final SortedMap<Integer, String> names;
  // 该方法入参中是否含有@Param注解
  private boolean hasParamAnnotation;

  /**
   * 参数名解析器的构造方法
   * @param config 配置信息
   * @param method 要被分析的方法
   */
  public ParamNameResolver(Configuration config, Method method) {
    // 获取参数类型列表
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 准备存取所有参数的注解，是二维数组
    // void queryByUserName(@Param("userName") @Autowire String userName)
    // lynch 比如：[0][0] = @Param("userName")  [0][1] = @Autowire
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    //lynch length等于参数个数
    int paramCount = paramAnnotations.length;
    // 循环处理各个参数
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        //lynch 这里可以点击进去看所谓的特别参数，一个是作用于分页，一个是作用于ResultHandler可以对返回的数据做一些处理
        //lynch 比如你写： queryName(String name,RowBounds rowBounds,String age)，那么最后这个name的下标是0，age的下标是2
        if (isSpecialParameter(paramTypes[paramIndex])) {
          // 跳过特别的参数
          continue;
        }
        // 参数名称
        String name = null;
        //lynch 这里在寻找当前这个参数的 @Param注解
        for (Annotation annotation : paramAnnotations[paramIndex]) {
          // 找出参数的注解
          if (annotation instanceof Param) {
            // 如果注解是Param
            hasParamAnnotation = true;
            // 那就以Param中值作为参数名
            name = ((Param) annotation).value();
            break;
          }
        }

        if (name == null) {
          // 否则，保留参数的原有名称
          //lynch 这里：是否使用原始参数名称，这里默认值是true，即使用，但是由于 String userName 编译后会变成 String arg0,所以这里即使开启了，拿到的也是arg0，除非你在编译的时候 javac -parameters xxx.java 文件才可以解决这个问题
          //lynch 那就说明了一个事情，如果你编译完成一个jar包，啥都不配置，你的mybatis是读不到多个参数的，因为他不知道参数的对应关系，他不知道username，他只知道arg0，arg1，arg3
          //lynch 如果有【多个】参数的时候，你不给配置@Param("userName")String userName,那么这个参数就无法映射到#{userName}的
          if (config.isUseActualParamName()) {
            //lynch 所以这里就很简单的通过反射机制拿到方法上的参数名称
            //lynch 如果想拿到预期的参数，意味着，没有配置@Param，且isUseActualParamName得是ture（默认true），且编译的时候你得指定保留原变量名
            name = getActualParamName(method, paramIndex);
          }
          if (name == null) {
            // 参数名称取不到，则按照参数index命名
            // lynch 有机会走到这里的，因为如果你config.isUseActualParamName()设置为false的话，那这里就是  0 1 2
            name = String.valueOf(map.size());
          }
        }
        map.put(paramIndex, name);
    }
    //lynch sortMap 会按照键自然顺序（升序）进行排序（这里显然就是根据paramIndex大小进行 0 1 （2假设2是特殊参数rowBounds则会被跳过） 3进行排序）
    //lynch 这里是有必要的，因为Map本身是无序的，可以统计处所有Map的key，然后按照大小排出 param1 param2 但是如果这样的话很麻烦，还不如这里直接就排好
    names = Collections.unmodifiableSortedMap(map);
  }

  private String getActualParamName(Method method, int paramIndex) {
    return ParamNameUtil.getParamNames(method).get(paramIndex);
  }

  private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
  }

  /**
   * Returns parameter names referenced by SQL providers.
   * 给出被解析的方法中的参数名称列表
   */
  public String[] getNames() {
    return names.values().toArray(new String[0]);
  }

  /**
   * <p>
   * A single non-special parameter is returned without a name.
   * Multiple parameters are named using the naming rule.
   * In addition to the default names, this method also adds the generic names (param1, param2,
   * ...).
   * </p>
   *
   * 将被解析的方法中的参数名称列表与传入的`Object[] args`进行对应，返回对应关系。
   *
   *
   * 如果只有一个参数，直接返回参数
   * 如果有多个参数，则进行与之前解析出的参数名称进行对应，返回对应关系
   * lynch  这里其实就是返回一个Map，里面是  参数名称->用户输入的参数值
   * lynch  其中，在末尾我们可以注意到，参数会被存两遍，第一遍是  参数名称->参数值  同时会马上存入  param1->参数值
   */
  //lynch 这里的输入参数是：["广东海洋大学",rowBounds对象,"陈奕迅","1"]
  //lynch 假设names的值是 0:school  2:userName  3:sex
  //lynch  最终得到的对象就是： school:广东海洋大学  userName:陈奕迅  sex:1
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      //lynch 如果只是一个参数，且没有@Param，那就比较简单，输入参数就必定是第一个位置的参数
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<>();
      //lynch  这个i说明了上面使用SortMap的重要性，不然这里的i很难计算出来，可能会比较麻烦 param1 param2 就这个后面的数字很难计算出来到底
      //lynch 如果想不用sortMap，这里应该需要去计算当前key在  0:school  2:userName  3:sex Map本身是无序的，你需要拿到所有key，然后再排序，算出2是param1，这样很麻烦
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // 首先按照类注释中提供的key,存入一遍   【参数的@Param名称 或者 参数排序：实参值】
        // 注意，key和value交换了位置
        //lynch  schoolName -> 广东海洋大学 这里就是把参数的名字（有可能是叫arg0等等），对应的值，存入到返回值Map中
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        // 再按照param1, param2, ...的命名方式存入一遍
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }
}
